﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Ncqrs.CommandExecution;
using Ncqrs.CommandExecution.AutoMapping;
using Ncqrs.Commands;

namespace Ncqrs.CommandHandling.Dispatching
{
    /// <summary>
    ///   A dispatcher that dispatch command objects to their appropriate command executor. Command executors can subscribe and
    ///   unsubscribe to specific command types. Only a single executor may be subscribed for a single type of command at any time.
    /// </summary>
    public class InProcessCommandExecutionDispatcher : CommandExecutionDispatcher
    {
        private readonly Dictionary<Type, ICommandExecutor> _executors = new Dictionary<Type, ICommandExecutor>();

        /// <summary>
        ///   Registers the executor for the specified command type. The executor will be called for every command of the specified type.
        /// </summary>
        /// <typeparam name = "TCommand">The type of the command.</typeparam>
        /// <param name = "executor">The executor that will be called for every command of the specified type.</param>
        /// <exception cref = "ArgumentNullException">Occurs when the <i>commandType</i> or <i>executor</i> was a <c>null</c> dereference.</exception>
        public void RegisterExecutor<TCommand>(ICommandExecutor executor) where TCommand : ICommand
        {
            RegisterExecutor(typeof (TCommand), executor);
        }

        /// <summary>
        ///   Registers the executor for the specified command type. The executor will be called for every command of the specified type.
        /// </summary>
        /// <param name = "commandType">Type of the command.</param>
        /// <param name = "executor">The executor that will be called for every command of the specified type.</param>
        /// <exception cref = "ArgumentNullException">Occurs when the <i>commandType</i> or <i>executor</i> was a <c>null</c> dereference.</exception>
        public void RegisterExecutor(Type commandType, ICommandExecutor executor)
        {
            _executors.Add(commandType, executor);
        }

        /// <summary>
        ///   Unregisters the executor of the specified command type. The executor will not be called any more.
        /// </summary>
        /// <param name = "commandType">Type of the command.</param>
        /// <param name = "executor">The executor to unregister.</param>
        /// <exception cref = "ArgumentNullException">Occurs when the <i>commandType</i> or <i>executor</i> was a <c>null</c> dereference.</exception>
        /// <exception cref = "InvalidOperationException">Occurs when the <i>executor</i> is not the same as the registered executor for the specified command type.</exception>
        public void UnregisterExecutor(Type commandType, ICommandExecutor executor)
        {
            ICommandExecutor registeredExecutor = null;

            if (_executors.TryGetValue(commandType, out registeredExecutor))
            {
                if (executor != registeredExecutor)
                {
                    throw new InvalidOperationException(
                        "The specified executor does not match with the registered executor for command type " +
                            commandType.FullName + ".");
                }

                _executors.Remove(commandType);
            }
        }

        /// <summary>
        ///   Gets the command executor for command.
        /// </summary>
        /// <param name = "commandType">Type of the command.</param>
        /// <returns>
        ///   A command executor to use to execute the command or <c>null</c> if not found.
        /// </returns>
        protected override ICommandExecutor GetCommandExecutorForCommand(Type commandType)
        {
            ICommandExecutor result = null;
            _executors.TryGetValue(commandType, out result);

            return result;
        }

        public void RegisterExecutorForAllCommandsIn(Assembly commandAssembly, ICommandExecutor executor)
        {
            var commands =
                from type in commandAssembly.GetTypes()
                where IsConcreteCommand(type)
                select type;

            foreach (var command in commands)
            {
                RegisterExecutor(command, new AutoMappingCommandExecutor());
            }
        }

        private static bool IsConcreteCommand(Type type)
        {
            return !type.IsAbstract && type.GetInterfaces().Any(t => t == typeof(ICommand));
        }
    }
}